Soracom Airで繋がったデバイスにリモートからSSHする
解決したいこと
IoT ソリューションで遠隔地にゲートウェイを設置したとします。 ゲートウェイからは Soracom Air で外部へ通信するのとして、逆方向、つまり、パブリック IP が振られたサーバからパブリック IP が振られていないゲートウェイに SSH する方法を紹介します。
システム全体図
Soracom AIRでインターネットに接続できるゲートウェイとパブリック IP が振られた Linux サーバ(以下「リモートサーバ」と呼びます)を用意します。
手順
ゲートウェイからリモートサーバへのSSH
リモートサーバの特定のポートをゲートウェイ(localhost)の SSH ポート(22)にフォワードするよう、リモートポートフォワードします。
GATEWAY_SERVER $ ssh -R REMOTE_PORT:localhost:22 remote-user@REMOTE_SERVER
リモートサーバからゲートウェイへのSSH
フォワードされているポート(REMOTE_PORT)に SSH すると、ゲートウェイに SSH できます。
REMOTE_SERVER $ ssh gateway-user@localhost -p REMOTE_PORT
ポートフォワードの手順を詳細に解説
SSH 時のポートフォワード向けオプション
今回は SSH をポートフォワード目的でのみ使い、 SSH 先でコマンド実行するわけではないので -N
オプションをつけます。
バックグラウンド実行させる -f
オプションを有効にします。
また、ポートフォワードが失敗しては意味が無いので、ポートフォワード失敗時にはプロセス終了する ExitOnForwardFailure=yes
のオプションを有効にします。
ExitOnForwardFailure オプションを有効にしていると、ポートがリモートですでに LISTEN しているなどしてポートフォワード出来ない場合
Error: remote port forwarding failed for listen port 4321
というエラーが発生して SSH プロセスが終了します。
SSH 時コネクション向けオプション
SSH コネクションが確立されているかチェックするために、一定間隔(ServerAliveInterval=60
)でメッセージを送信し、指定回数(ServerAliveCountMax=3
)応答がなければ、切断するようにします。
SSH 先のホストチェック向けオプション
初めて SSH する場合やIPが同じだけれどもフィンガープリントが変わった場合、インタラクティブな確認が求められます。 SSH 先は安心できるホストだという前提のもと、SSH 時にこれらのチェックをスキップするために以下のオプションを追加します。
StrictHostKeyChecking=no
(フィンガープリントのチェックをしない)UserKnownHostsFile=/dev/null
(接続先のフィンガープリントを 保存しない)
SSH コマンド全体
ゲートウェイのSSHコマンド
以上をまとめると、ゲートウェイが実行する SSH コマンドは以下の様になります。
$ ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ServerAliveInterval=60 \ -o ServerAliveCountMax=3 \ -o ExitOnForwardFailure=yes \ -i /root/.ssh/ops.pem \ -N \ -f \ -R 8888:localhost:22 [email protected]
リモートサーバのSSHコマンド
トンネルを掘った後は、リモートサーバが実行する SSH コマンドは以下です。
$ ssh localhost -p 8888
SSH オプションを設定ファイルに引っ越す
リモートポートフォワードはコマンドオプションが長いので、設定ファイル(~/.ssh/config
)に引っ越します。設定ファイルのパーミッションは自分だけが読み書きできるように設定します($ chmod 600 ~/.ssh.config)
Host ops HostName 55.66.77.88 User ec2-user IdentityFile ~/.ssh/ops.pem StrictHostKeyChecking no UserKnownHostsFile /dev/null ServerAliveInterval 60 ServerAliveCountMax 3 ExitOnForwardFailure yes RemoteForward 8888 localhost:22
このように設定しておくと、ゲートウェイからはシンプルに
$ ssh -N -f ops
と実行するだけです。
接続が切れた時に再接続させる
ネットワーク障害やリモートサーバーの再起動などでSSH接続が切断することが考えられます。
autossh
を使うと、SSH 切断時に再接続の面倒をみてくれます。自前で再接続のロジックをシェルスクリプトで書くよりもずっと確実です。
autossh のインストール
URL http://www.harding.motd.ca/autossh/
Amazon Linux
$ sudo yum install -y autossh --enablerepo=epel
Ubuntu
$ sudo apt-get install -y autossh
autossh のオプション
コマンドは簡単で、 $ ssh OPTIONS
に対して $ autossh -M 0 OPTIONS
とするだけです。
今回の場合は、元の SSH コマンドが $ ssh -N -f ops
だったため $ autossh -M 0 -N -f ops
となります。
autossh には独自にSSHコネクションをモニタリングする機能が存在します。
man ページを読むと、autossh 標準のモニタリングより OpenSSH のモニタリングを推奨しているため、autossh のモニタリングを無効化する -M 0
オプションを渡しています。
autossh の Man ページから引用します。
Setting the monitor port to 0 turns the monitoring function off, and autossh will only restart ssh upon ssh's exit. For example, if you are using a recent version of OpenSSH, you may wish to explore using the ServerAliveInterval and ServerAliveCountMax options to have the SSH client exit if it finds itself no longer connected to the server. In many ways this may be a better solution than the monitoring port.
autossh の再接続確認
リモートサーバーを意図的に再起動させるなどしてトンネル用接続を切断させてみました。
Dec 20 12:52:25 ip-172-*-*-* autossh[2392]: port set to 0, monitoring disabled Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: starting ssh (count 1) Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: ssh child pid is 2394 Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: ssh exited with error status 255; restarting ssh Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: starting ssh (count 2) Dec 20 12:52:25 ip-172-*-*-* autossh[2393]: ssh child pid is 2395
- ssh exited with error status 255; restarting ssh
- starting ssh (count 2)
などと SSH の切断を検知して、再接続していることがわかりますね。
ログは /var/log/messages
から取りました。
ぷらっとホーム OpenBlocks BX1 起動時にリモートサーバに SSH させる
/etc/rc.local
に autossh コマンドを追記するだけです。
#!/bin/sh -e # # rc.local # # This script is executed at the end of each multiuser runlevel. # Make sure that the script will "exit 0" on success or any other # value on error. # # In order to enable or disable this script just change the execution # bits. # # By default this script does nothing. autossh -N -f ops # XXX : ADDED LINE exit 0
リモートサーバー情報を後から変更する
やんごとなき理由により
- リモートサーバーの IP が変わった
- リモートサーバーの SSH 鍵が変わった
といったことがあるかもしれません。 ゲートウェイ設置後にリモートサーバーへの接続情報を変更する方法を考えてみましょう。
Soracom Air のメタデータ について
SORACOM Air には「メタデータサービス」という機能が存在します。
AWS EC2 のメタデータのように、SIM から接続している場合のみ、その SIM に属するメタデータを読み書きするサービスです。
SORACOM Air メタデータサービスのご紹介 - SORACOM Blog
メタデータサービスは標準では無効化されており、グループ単位で有効・無効にします。 有効にすると、SIM 情報や通信に関する情報などを取得出来ます。
Soracom Air のユーザーデータでリモートサーバ情報を連携
AWS EC2 と同じく、ユーザー自身がデータをカスタマイズできるユーザーデータ機能もサポートしています。
このユーザーデータ欄にリモートサーバの IP や SSH 鍵などの情報を登録しておき、ゲートウェイが定期的にユーザーデータを参照して同期するようにしておけば、リモートサーバが変更になっても、SSH できます。
目指すところは AWS IoT Device Shadows と同じかと思います。
SSH 先が複数あるケースも考慮すると、ユーザーデータは次の様なデータ構造が良いでしょうか。
{ "ops": { "KeyMaterial": "-----BEGIN RSA PRIVATE KEY-----\nMII...SNIP...hE=\n-----END RSA PRIVATE KEY-----", "HostName": "11.22.33.44", "RemotePort": 8080 } }
SORACOM管理画面から設定します。
ユーザーデータの取得は AWS EC2 と同じく、特定のエンドポイントを叩くだけです。
$ curl -s http://metadata.soracom.io/v1/userdata { "ops": { "KeyMaterial": "-----BEGIN RSA PRIVATE KEY-----\nMII...SNIP...hE=\n-----END RSA PRIVATE KEY-----", "HostName": "11.22.33.44", "RemotePort": 8080 } }
レスポンスの JSON をパースして SSH 鍵や ~/.ssh/config
ファイルを再生成するプログラムを用意してやれば OK ですね。
SSH 接続先サーバのホストキーなどはチェックしないようにしているため、接続先が変わっても、確認を求められることはありません。
まとめ
クラウドだけをいじっている人は「SSH したら負けかなと思っている」などと(わざと)煽る人もいますが、現実問題として IoT ソリューションではリモートからデバイスの状態をモニターできるようにし、リモートからデバイスを管理できるようにしないと、運用で疲弊するだけです。
泥臭い IoT をより楽に回せるなアイデアがあれば、今後も共有していきたいと思います。
参照
- https://blog.soracom.jp/blog/2015/11/27/air-metadata/
- https://www.compose.io/articles/autossh-for-persistent-database-connectivity/
- http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
- http://docs.aws.amazon.com/iot/latest/developerguide/iot-thing-shadows.html
- http://www.harding.motd.ca/autossh/